package com.gizwits.datapoint

import spray.json._
import ProtoDefJsonProtocol._

trait MQTTLength {
  def calcBytesOfLength(length: Int) = {
    if (length >= 0 && length < 128) 1
    else if (length >= 128 && length < 16384) 2
    else if (length >= 16384 && length < 2097152) 3
    else if (length >= 2097152 && length < 268435456) 4
    else 0
  }

  def dynamicLength(bytes: List[Byte], offset: Int) = {
    def getLengths(bytes:Seq[Byte], offset:Int, multiplier:Int) : Stream[Int] = {
      val b = bytes(offset)
      val length = (b & 127) * multiplier
      if ((b & 128) != 0) {
        length #:: getLengths(bytes, offset + 1, multiplier * 128)
      } else {
        length #:: Stream.empty
      }
    }
    val s = getLengths(bytes, offset, 1)
    (s.sum, s.length)
  }

  def lengthToBytes(length: Int): List[Byte] = {
    def to(length: Int): Stream[Byte] = {
      if (length > 0) {
        val left = length / 128;
        val digit = if (left > 0) {
          (length % 128) | 0x80
        } else (length % 128)
        digit.toByte #:: to(left)
      } else Stream.empty
    }
    to(length).take(4).toList
  }

}

sealed abstract class Protocol(val protoDef: ProtoDef) extends MQTTLength {
  lazy val cmdLength = protoDef.commands.head._2.cmd.length
  def pack(cmdName: Symbol, values: Map[Symbol, Any] = Map()): List[Byte] = {
    cmdBytes(cmdName) ::: packData(cmdName, values)
  }

  def packData(cmdName: Symbol, values: Map[Symbol, Any] = Map()): List[Byte] = {
    def propData(prop: Property): List[Byte] = {
      val isOptional = prop.optional getOrElse false
      if ((isOptional && values.contains(prop.name)) || !isOptional) {
        val value = values(prop.name)
        prop.write(value)
      } else Nil
    }
    val optProps = protoDef.commands(cmdName).props
    val data = optProps match {
      case Some(props) => props.map(propData).flatten.toList
      case None => Nil
    }
    data
  }

  def unpack(cmd: Command, data: Array[Byte]): Map[Symbol, Any] = {
    val buffer = java.nio.ByteBuffer.wrap(data)
    def readData(prop: Property) : Option[(Symbol, Any)] = {
      prop.read(buffer, 0)
    }
    cmd.props match {
      case Some(props) => props.map(readData).filter(!_.isEmpty).map(_.get).toMap
      case None => Map[Symbol, Any]()
    }
  }

  def unpack(bytes: List[Byte]) : Option[(Symbol, Map[Symbol, Any])]

  def headerBytes(): List[Byte] = protoDef.header
  def cmdBytes(name: Symbol): List[Byte] = protoDef.commands(name).cmd
  def cmdFrom(bytes: List[Byte]): Option[(Symbol, Command)] = {
    protoDef.commands.find(p => p._2.cmd == bytes)
  }
}

class App2Dev(protoDef: ProtoDef) extends Protocol(protoDef) {
  lazy val flag: Byte = 0
  lazy val fixHeadLength: Int = headerBytes.length + 1
  override def pack(cmdName: Symbol, values: Map[Symbol, Any] = Map()): List[Byte] = {
    val data = flag :: super.pack(cmdName, values)
    val varLength = lengthToBytes(data.length)
    headerBytes ::: varLength ::: data
  }

  override def unpack(bytes: List[Byte]) : Option[(Symbol, Map[Symbol, Any])] = {
    assert(headerBytes == bytes.take(headerBytes.length))
    val (varLength, bytesOfLength) = dynamicLength(bytes, headerBytes.length)
    assert(flag == bytes(headerBytes.length + bytesOfLength))
    val cmdIndex = fixHeadLength + bytesOfLength
    val optCmd = cmdFrom(bytes.slice(cmdIndex, cmdIndex + cmdLength))
    val data = bytes.drop(cmdIndex + cmdLength)
    optCmd match {
      case Some((name, command)) => Option((name, super.unpack(command, data.toArray)))
      case None => Option(null)
    }
  }
}

class App2Cloud(protoDef: ProtoDef) extends App2Dev(protoDef)

class Agent2Mcu(protoDef: ProtoDef, rollingSeq: Boolean = true) extends Protocol(protoDef) {
  lazy val flag: List[Byte] = List(0,0)
  var seq: Byte = 1
  def nextSeq: Byte = if (rollingSeq) {
    while(seq == 0) seq = (seq + 1).toByte
    seq
  } else 1
  def calcChecksum(bytes: List[Byte]): Byte = {
    (bytes.map(_ & 0xff).sum % 256).toByte
  }

  def writeInt(value: Int, length: Int): List[Byte] = {
    val buffer = java.nio.ByteBuffer.allocate(length)
    length match {
      case 1 => buffer.put(value.toByte)
      case 2 => buffer.putShort(value.toShort)
      case 4 => buffer.putInt(value)
      case _ => return Nil
    }
    buffer.array.toList
  }

  override def pack(cmdName: Symbol, values: Map[Symbol, Any] = Map()): List[Byte] = {
    val body = cmdBytes(cmdName) ::: nextSeq :: flag ::: super.packData(cmdName, values)
    val data = writeInt(body.length + 1, 2) ::: body
    val checksum = calcChecksum(data)
    headerBytes ::: (data :+ checksum)
  }

  override def unpack(bytes: List[Byte]) : Option[(Symbol, Map[Symbol, Any])] = {
    assert(headerBytes == bytes.take(headerBytes.length))
    val bytesOfLength = 2
    val varLength = {
      val arr = bytes.slice(headerBytes.length, headerBytes.length + 2).toArray
      java.nio.ByteBuffer.wrap(arr).getShort
    }
    val flagIndex = headerBytes.length + bytesOfLength + cmdLength + 1
    assert(flag == bytes.slice(flagIndex, flagIndex + flag.length))
    val cmdIndex = headerBytes.length + bytesOfLength
    val seq = bytes(cmdIndex + cmdLength)
    val optCmd = cmdFrom(bytes.slice(cmdIndex, cmdIndex + cmdLength))
    val checksumValid = calcChecksum(bytes.slice(headerBytes.length, bytes.length - 1)) == bytes.last
    assert(checksumValid)
    val dataIndex = cmdIndex + cmdLength + 1 + flag.length
    val data = bytes.slice(dataIndex, bytes.length - 1)
    optCmd match {
      case Some((name, command)) => Option((name, super.unpack(command, data.toArray)))
      case None => Option(null)
    }
  }
}

